<?php
/* vim: set expandtab tabstop=2 shiftwidth=2 softtabstop=2: */
/**
* class.XiTemplate.php
*
* PHP version 5.X REQUIRED
*
* This class file runs with error_reporting(E_STRICT) without creating any
* E_NOTICE's.
*
* XiTemplate is based on Xtemplate by Barnabas Debreceni.
* XiTemplate has been ported to take advantage of the PHP 5 object model,
* which adds speed and more accurate handling of member variables and methods
* within the class. Several new features are introduced like the ability to
* pass a template from the buffer as well as from a file and the ability to
* dynamically interpolate multidimensional arrays.
* An attempt was also made to reduce the memory foot print and increase
* performance.  The performance gain comes during higher load situation
* where there is less free memory.
*
* @package XiTemplate
*/
/**
* XiTemplate is a very fast and very easy to use PHP template engine that
* enables a developer to completely seperate logic from presentation.
*
* @package XiTemplate
* @version 1.0.1
* @author Travis Miller <tmiller@web-1hosting.net>
* @author Jim Grill <jimgrill@jimgrill.com>
* @copyright Copyright (c) 2004, Travis Miller, Jim Grill
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @tutorial XiTemplate.cls
*/
class XiTemplate
{
  /**
  * @desc raw contents of template file
  */
	private $fileContents = '';

	/**
	* @desc unparsed blocks
	*/
	private $blocks       = array();

	/**
	* @desc parsed blocks
	*/
	private $parsedBlocks = array();

	/**
	* @desc store sub-block names for fast resetting
	*/
	private $subBlocks    = array();

	/**
	* @desc variables array
	*/
	private $VARS         = array();

	/**
	* auto-reset sub blocks
	*/
	private $AUTORESET    = true;

	/**
	* @desc what to assign to unassigned template variables. Defaults to ''.
	* Sometimes a null value is desired, but if you are parsing a table
	* and a null value populates a cell this can cause problems. In this case
	* setting $nullValue to &nbsp; might be a better idea.
	*
	* Note: Sometimes it may be desireable to set a null value temporarily.
	* To achieve this simply reset null values to ''.
	*
	* @var string $value
	* @example output/nullValue.html Resulting HTML after parsing
	* @example templates/nullValue.tpl HTML Template used for this example
	* @example nullValue.php Example usage of $nullValue
	*/
	public $nullValue       = '';


	/**
	* @return object $XiTemplate
	* @param string $template
	* @desc XiTemplate constructor, loads the template text from file or buffer
	* and instantiates the XiTemplate object.
	* @example output/construct.html Resulting HTML after parsing
	* @example templates/construct.tpl HTML template used for this example
	* @example construct.php Example usage of __construct()
	*/
	function __construct($template)
	{
	  /*check to see if the template comes from the buffer*/
	  /*by looking for "<!-- BEGIN tags"*/
    if (preg_match('/\<!--\sBEGIN/', $template))
			$this->fileContents = $template;
		else
			$this->_recursiveReadFile($template);

		$this->_makeTree();
	}

/******************************************************************************/
/* private methods                                                            */
/******************************************************************************/

  /**
	* @return void
	* @param string $msg
	* @desc Issues a warning via trigger_error()
	*/
	private function logWarning($msg)
	{
		trigger_error($msg, E_USER_WARNING);
	}

	/**
	* @return void
	* @param string $msg
	* @desc Issues a fatal error via trigger_error()
	*/
	private function logError($msg)
	{
		trigger_error($msg, E_USER_ERROR);
		exit;
	}

	/**
	* @return string $fileText
	* @param string $fileName
	* @desc Read the contents of $fileName.
	*/
	private function _readFile($fileName)
	{
		if (!is_file($fileName))
			$this->logError($fileName . ' does not exist.');

		if (($fileText = file_get_contents($fileName)) === false)
			$this->logError($fileName . ' is not readable.');

		return($fileText);
	}

	/**
	* @return void
	* @param string $fileName
	* @desc Read the contents of $fileName in a recursive manner such that
	*	FILE blocks are also included.
	*/
	private function _recursiveReadFile($fileName)
	{
		/*Read main file contents*/
		$this->fileContents = $this->_readFile($fileName);
		$matches = array();

		/*Read FILE: includes*/
		while (preg_match('/\{FILE\s*\"([^\"]+)\"\s*\}/m', $this->fileContents, $matches))
		{
			$includedFile = $this->_readFile($matches[1]);
			$this->fileContents = preg_replace('\'' . preg_quote($matches[0]) . '\'',
				$includedFile, $this->fileContents);
		}
	}

	/**
	* @return void
	* @desc Parse all the blocks out of $this->fileContents.
	*/
	private function _makeTree()
	{
		/*Initialize variables.*/
		$level      = 0;
		$blockNames = array();
		$matches    = array();

		/*Find all the delimiters*/
		$delimiters = explode('<!-- ', $this->fileContents);

		/*Loop through checking for begining or ending delimiter and manipulate*/
		/*the tree as needed.*/
		foreach($delimiters as $v)
		{
			if (preg_match_all('/^(BEGIN:|END:)\s*(\w+)\s*-->(.*)/ims', $v, $matches, PREG_SET_ORDER))
			{
				/* $matches[0][1] = BEGIN or END*/
				/* $matches[0][2] = block name*/
				/* $matches[0][3] = kinda content*/
				if ($matches[0][1] == 'BEGIN:')
				{
					/*If we have a BEGIN: block, add it to blocks list.*/
					$parentName = implode('.', $blockNames);
					$blockNames[++$level] = $matches[0][2];
					$currentBlockName = implode('.', $blockNames);
					$this->blocks[$currentBlockName] = isset($this->blocks[$currentBlockName]) ?
							$this->blocks[$currentBlockName] . $matches[0][3] :
							$matches[0][3];
					$this->blocks[$parentName] .= '{_BLOCK_.' . $currentBlockName . '}';
					$this->subBlocks[$parentName][] = $currentBlockName;
					$this->subBlocks[$currentBlockName][] = "";
				}
				elseif ($matches[0][1] == 'END:' )
				{
					/*If we have an END block, reduce our parent list.*/
					unset($blockNames[$level--]);
					$parentName = implode('.', $blockNames);

					/*Add rest of block to parent block*/
					$this->blocks[$parentName] .= $matches[0][3];
				}
			}
			else
			{
				$index = implode('.', $blockNames);
				$this->blocks[$index] = (isset($this->blocks[$index])) ?
						($this->blocks[$index] . '<!-- ' . $v) : '<!-- ' . $v;
			}
		}
	}

	/**
	* @return void
	* @param array $array
	* @param string $templateVariable
	* @desc Integrate an array, regardless of dimension, into the VARS array.
	*/
	private function _assignArray($array, $templateVariable)
	{
		/*Recursively reduce the array until we don't have an array as the value,*/
		/*then store the path of the array key to the value.*/
		if (is_array($array))
		{
			foreach($array as $k=>$v)
				$this->_assignArray($v, $templateVariable. '.' . $k);
		}
		else
		{
			$this->VARS[$templateVariable] = $array;
		}
	}


/******************************************************************************/
/* public methods                                                             */
/******************************************************************************/

	/**
	* @return void
	* @param string $templateVariable
	* @param mixed $value
	* @desc Assign a value to a template variable. $value can be either a
	* string or a single or multidimensional array.
	* @example output/assign.html Resulting HTML after parsing
	* @example templates/assign.tpl HTML Template used for this example
	* @example assign.php Example usage of assign()
	*/
	public function assign($templateVariable, $value='')
	{
		/*If assigning an array, use special function to*/
		/*allow multidimensional arrays.*/
		if (is_array($value))
			$this->_assignArray($value, $templateVariable);
		/*Otherwise, just stick it in the list.*/
		else
			$this->VARS[$templateVariable] = $value;
	}

	/**
	* @return void
	* @param string $blockName
	* @desc Parse a block of the template replacing parsed blocks and variables.
	* @example output/parse.html Resulting HTML after parsing
	* @example templates/parse.tpl HTML template used in this example
	* @example parse.php Example usage of parse()
	*/
	public function parse($blockName='main')
	{
		if (!isset($this->blocks[$blockName]))
		{
			$this->logError('Template::parse() - block [' .
				$blockName . '] does not exist.');
		}

		/*Save a copy of the old block so that it can be appended to.*/
		$blockCopy = $this->blocks[$blockName];

		/*Find all the variables in this block.*/
		$varArray = array();
		preg_match_all('/\{([A-Za-z0-9\._]+?)}/',
			$this->blocks[$blockName], $varArray);
		$varArray = $varArray[1];

		/*For each variable, determine if it represents a block or an assigned var, then*/
		/*replace it accordingly.*/
		foreach($varArray as $v)
		{
			if (strncmp($v, '_BLOCK_', 7) == 0)
			{
				/*We are dealing with a parsed block.*/
			  $parsedBlockName = substr($v, 8);

			  /*If it has been parsed, replace it with the parsed text, else leave it blank.*/
				$var = (isset($this->parsedBlocks[$parsedBlockName])) ?
					trim(($this->parsedBlocks[$parsedBlockName])) :	'';
				/*str_replace is faster and fixes the old Xtemplate US$ bug #440545*/
				$blockCopy = str_replace('{' . $v . '}', $var, $blockCopy);
			}
			else
			{
				/*If variable has been assigned, replace it, else assign it*/
				/*our default $nullValue.*/
				//$var = (isset($this->VARS[$v]) && !empty($this->VARS[$v])) ?
				/*Fix Empty Value*/
				 $var = isset($this->VARS[$v]) ?
				  $this->VARS[$v] : $this->nullValue;
				/*str_replace is faster and fixes the old Xtemplate US$ bug #440545*/
				$blockCopy = str_replace('{' . $v . '}', $var, $blockCopy);
			}
		}
		/*Replace the old parsed block with itself plus the new data.*/
		$this->parsedBlocks[$blockName] = (isset($this->parsedBlocks[$blockName])) ?
				$this->parsedBlocks[$blockName] . $blockCopy : $blockCopy;

		/*reset sub-blocks*/
		if ($this->AUTORESET && (!empty($this->subBlocks[$blockName])))
		{
			reset($this->subBlocks[$blockName]);
			foreach($this->subBlocks[$blockName] as $subBlock)
				$this->reset($subBlock);
		}
	}

	/**
	* @return void
	* @param string $blockName
	* @desc Parse a block of the template and all sub-blocks under it.
	* @example output/rparse.html Resulting HTML after parsing
	* @example templates/rparse.tpl HTML template used in this example
	* @example rparse.php Example usage of rparse()
	*/
	public function rparse($blockName='main')
	{
		if (!empty($blockName))
		{
			reset($this->subBlocks[$blockName]);
			foreach($this->subBlocks[$blockName] as $block)
				if (!empty($block))	$this->rparse($block);
		}
		$this->parse($blockName);
	}

	/**
	* @return void
	* @param string $blockName
	* @desc Resets a parsed block to empty.
	*/
	public function reset($blockName)
	{
		if (!isset($this->parsedBlocks[$blockName]))
		  return;

		$this->parsedBlocks[$blockName] = '';
	}

	/**
	* @return string parsedBlock
	* @param string $blockName
	* @desc Returns parsed block of template.
	*/
	public function text($blockName='main')
	{
		if (sizeof($this->parsedBlocks) == 0)
			$this->logError('Cannot get template content,
				main block has yet to be parsed');

	  if (!isset($this->parsedBlocks[$blockName]))
      $this->logError('Block: "'. $blockName .'" does not exist');

		return($this->parsedBlocks[$blockName]);
	}

	/**
	* @return void
	* @param string $blockName
	* @desc Outputs a parsed block of a template to the browser.
	* Will generate a fatal error if $blockName does not exist in the
	* template instance.
	* @example output/out.html Resulting HTML after parsing
	* @example templates/out.tpl HTML template used in this example
	* @example out.php Example usage of out()
	*/
	public function out($blockName='main')
	{
		echo($this->text($blockName));
	}
}
?>